<?php
declare(strict_types=1);

namespace lib\pay;

use app\index\BootStarup;
use lib\pay\helpers\PaySign;
use Swoole\Coroutine;
use sys\Log;

class XinPay extends PayBase {

    public int $id;
    public int $mid;
    public float $amount;
    public float $finnal_amount;
    public float $refund_amount;
    public int $refund_times;
    public int $order_type;
    public int $order_id;
    public string $trade_no;
    public string $out_trade_no;
    public string $buyer_id;
    public string $chn;
    public int $stage;
    public int $datetime;
    public int $expire;
    public string $errmsg;


    # 产品用
    protected static string $org_code       = '2211596123';
    protected static string $mch_id         = '849557748160003';

    protected static string $base_url_dev   = 'https://qrcode-dev.jlpay.com';
    protected static string $base_url       = 'https://qrcode.jlpay.com';

    protected static array $propNamesC = ['id', 'mid', 'amount', 'finnal_amount', 'refund_amount', 'refund_times', 'order_type', 'order_id','trade_no','out_trade_no','buyer_id', 'chn', 'stage', 'datetime', 'expire'];
    protected static array $propNamesU = ['refund_times', 'refund_amount', 'finnal_amount', 'order_type', 'order_id', 'stage', 'buyer_id', 'datetime', 'timeout', 'errmsg'];


    /**
     *  付款码收款接口
     * @access public
     * @param float $total_fee 总金额
     * @param string $auth_code 授权码
     * @param string $ipaddr 终端IP地址
     * @param int $timeout 超时
     * @param int $mid 商户ID
     * @param string $body 标题
     * @param null|string $attach 描述
     * @param callable $callback 回调
     */
    public static function micropay(string $term_no, float $total_fee, string $auth_code, string $ipaddr, int $timeout, int $mid, string $body, ?string $attach = null, callable $callback = null) : ?static
    {
        # 接口地址:
        #   生产环境: https://qrcode.jlpay.com/api/pay/micropay
        #   测试环境: https://qrcode-dev.jlpay.com/api/pay/micropay
        #   文档地址: https://help.jlpay.com/api/docs/document/ba8503db6ee44464a96a9a5c67938fb7

        $abs_timeout = time() + $timeout;
        if(preg_match('/^1[012345]\d{16}$/', $auth_code)){
            # 微信
            $pay_type = Pay::SRC_WXAPP;
        }elseif(preg_match('/^(2[56789]|30)\d{14,22}$/', $auth_code)){
            # 支付宝
            $pay_type = Pay::SRC_ALIPAY;
        }elseif(preg_match('/^91\d{16}$/', $auth_code)){
            # QQ钱包
            $pay_type = Pay::SRC_QQPAY;
        }else{
            # 云闪付 ???
            $pay_type = Pay::SRC_UNIPAY;
        }
        $out_trade_no = \lib\Utils::genOrderNo('J');

        $biz = [
            'pay_type'              => $pay_type,                           # wxpay、alipay、unionpay、qqpay
            'payment_valid_time'    => strval($timeout),                    # 订单支付有效期,  超时后无法继续支付
            'body'                  => $body,                               # 商品名
            'attach'                => null == $attach ? '.':$attach,       # 商品描述
            'total_fee'             => strval(round(100 * $total_fee)),     # 总金额(单位:分)
            'auth_code'             => $auth_code,                          # 付款码
            'mch_create_ip'         => $ipaddr,                             # 接口调用机器的IP
            'out_trade_no'          => $out_trade_no,                       # 商户系统订单号
            'term_no'               => $term_no,                            # 终端号
        ];

        if(false === PaySign::IS_DEV_MODE){
            switch($pay_type){
            case Pay::SRC_WXAPP:
                $biz['sub_appid'] = PaySign::WEIAPP_ID;
                break;
            case Pay::SRC_WOA:
                $biz['sub_appid'] = PaySign::WOA_ID;
                break;
            case Pay::SRC_ALIPAY:
                $biz['sub_appid'] = PaySign::ALIPAY_ID;
                break;
            }
        }

        # 告诉客户端 还有多久超时
        $callback(['success' => true, 'data' => ['timeout'=>$timeout, 'message'=>'等待用户付款....']]);

        $result = static::call('/api/pay/micropay', $biz);

        if(null !== $result){
            $status = $result['status'] ?? -1;
            if($status == 1 || $status == 2){

                $trans_time = isset($result['trans_time'])? strtotime($result['trans_time']) :time();

                if(isset($result['finnal_amount'])){
                    $finnal_amount = intval($result['finnal_amount']) / 100;
                }else{
                    $finnal_amount = $total_fee;
                }

                $tp_nb_pays_xin = [
                    'mid'             => $mid,
                    'out_trade_no'    => $result['transaction_id'],   # 接口方的交易号
                    'trade_no'        => $out_trade_no,               # 我们的商户号
                    'amount'          => $total_fee,                  # 付款金额
                    'refund_times'    => 0,                           # 退款次数
                    'refund_amount'   => 0,                           # 退款金额
                    'finnal_amount'   => $finnal_amount,              # 实际支付金额
                    'order_type'      => 256,                         # 未知订单类型
                    'order_id'        => 0,                           # 支付记录关联的订单号
                    'stage'           => $status == 2 ?               # 是否完成付款
                                         static::STAGE_PAID:static::STAGE_PAYING,    
                    'chn'             => $result['pay_type'],         # 付款渠道 wxpay、alipay、unionpay、qqpay
                    'buyer_id'        => $result['sub_openid'] ?? '', # 微信侧的openid
                    'datetime'        => $trans_time,                 # 交易时间
                    'expire'          => $abs_timeout                 # 超时时间
                ];

                $db = new \sys\Db();
                $tp_nb_pays_xin['id'] = $db->table('tp_nb_pays_xin')->insert($tp_nb_pays_xin);
                $db = null;
                $pay = static::CreateInstance($tp_nb_pays_xin);

                if($pay->stage == static::STAGE_PAYING){
                    static::$micropay_cached[$auth_code] = [$pay, $channel = new \swoole\Coroutine\Channel(1)];
                    $callback(['success' => true, 'message'=>'等待用户付款....', 'data' => ['timeout'=>$abs_timeout - time()]]);
                    while($pay->stage == static::STAGE_PAYING) {
                        if(time() > $abs_timeout){
                            
                            $pay->lock();
                            $pay->cancel($ipaddr);
                            $pay->unlock();

                            unset(static::$micropay_cached[$auth_code]);
                            if(empty(static::$micropay_cached)){
                                static::$micropay_cached = [];
                            }

                            $pay->lock();
                            $channel->close();
                            $pay->unlock();

                            throw new PayException('等待支付超时.', 1003);
                        }
                        # 这里需要改用 channel 来实现.
                        $wait = min(3, max(1, $abs_timeout - time()));
                        Log::write("等待支付中... {$wait} 秒后再次查询", 'INFO');
                        if(true === $channel->pop($wait)){
                            if($pay->stage == static::STAGE_CLOSED){
                                throw new PayException('交易已取消!', 1004);
                            }
                        }

                        # 规定时间内没有取消付款, 则主动查询一次
                        if($channel->errCode == SWOOLE_CHANNEL_TIMEOUT){
                            $pay->lock();
                            $pay->query();
                            $pay->unlock();
                        }
                    }

                    unset(static::$micropay_cached[$auth_code]);
                    if(empty(static::$micropay_cached)){
                        static::$micropay_cached = [];
                    }
                    $channel->close();
                    
                    if($pay->stage == static::STAGE_PAID){   
                        return $pay;
                    }

                    $pay->lock();
                    $pay->cancel($ipaddr);
                    $pay->unlock();

                    throw new  PayException('等待支付超时.', 1003);
                } elseif($pay->stage == static::STAGE_PAID) {
                    return $pay;
                }
            }else{
                throw new  PayException($result['ret_msg'], 1002);
            }
        }
        throw new  PayException("调用网关接口发生错误.", 1001);        
    }

    public static function onNotify(array $params) : void {
        $status = $params['status'] ?? -1;
        if(2 == $status) {
            $remark = json_decode($params['remark'], true);
            $trade_no = $params['out_trade_no'];
            if(isset(static::$notify_inprogress[$trade_no])){
                return;
            }
            static::$notify_inprogress[$trade_no] = true;

            $total_fee = intval($params['total_fee']) / 100;
            $tp_nb_pays_xin = [
                'mid'             => $remark['mid'],
                'out_trade_no'    => $params['transaction_id'],   # 接口方的交易号
                'trade_no'        => $params['out_trade_no'],     # 我们的商户号
                'amount'          => $total_fee,                  # 付款金额
                'refund_times'    => 0,                           # 退款次数
                'refund_amount'   => 0,                           # 退款金额
                'finnal_amount'   => $total_fee,                  # 实际支付金额
                'order_type'      => 256,                         # 未知订单
                'order_id'        => 0,                           # 支付记录关联的订单号
                'stage'           => static::STAGE_PAID,         # 是否完成付款
                'chn'             => $params['pay_type'],         # 付款渠道 wxpay、alipay、unionpay、qqpay
                'buyer_id'        => $params['sub_openid'] ?? '', # 微信侧的openid
                'datetime'        => strtotime($params['trans_time']),    # 交易时间
                'expire'          => 0,
            ];
            $db = new \sys\Db();
            try{
                $tp_nb_pays_xin['id'] = $db->table('tp_nb_pays_xin')->insert($tp_nb_pays_xin);
                $pay = new XinPay($tp_nb_pays_xin);
            }catch(\Throwable $e){
                $err = [
                    'Error:'.$e->getMessage().' at file:'.$e->getFile().'('.$e->getLine().')',
                    "TraceBack:\n".$e->getTraceAsString()
                ];
                Log::write(implode("\n", $err), 'ERROR');
                $pay = null;
            }
            
            Log::console('支付成功: '.$trade_no, 'DEBUG');
            if(null !== ($channel = (static::$qrcodepay_cached[$trade_no] ?? null))){
                $channel->push($pay);
                Log::console('支付成功, 通知商户', 'DEBUG');
                unset(static::$qrcodepay_cached[$trade_no]);
                if(empty(static::$qrcodepay_cached)){
                    static::$qrcodepay_cached = [];
                }

                unset(static::$notify_inprogress[$trade_no]);
                if(empty(static::$notify_inprogress)){
                    static::$notify_inprogress = [];
                }

                return;
            }
            Log::console('通知商户失败, 开始退款.', 'DEBUG');
            $pay->refund($db, $total_fee, '222.242.224.119', '支付超时自动退款.');
            unset(static::$qrcodepay_cached[$trade_no]);
            if(empty(static::$qrcodepay_cached)){
                static::$qrcodepay_cached = [];
            }
        }
        return;
    }

    # 展示二维码给用户支付
    # API文档: https://help.jlpay.com/api/docs/document/f5b2fe670f2544ec9c09f9c9d5dd4a33

    public static function qrcodepay(string $term_no, float $total_fee, string $ipaddr, int $timeout, int $mid, string $body, ?string $attach = null, callable $callback = null) : ?static {
        # 二维码支付的订单号
        $out_trade_no = \lib\Utils::genOrderNo('Q');
        # 超时时间
        $abs_timeout = time() + $timeout;

        $biz = [
            'term_no'               => $term_no,                        # 终端号
            'pay_type'              => 'jlpay',                         # wxpay、alipay、unionpay、qqpay、jlpay
            'payment_valid_time'    => strval($timeout),                # 订单支付有效期,  超时后无法继续支付
            'body'                  => $body,                           # 商品名
            'attach'                => $attach,                         # 商品描述
            'remark'                => json_encode(['mid'=>$mid]),      # 需要在回调中透传的数据
            'out_trade_no'          => $out_trade_no,                   # 外部单号
            'total_fee'             => round(100 * $total_fee),         # 总金额(单位:分)
            'mch_create_ip'         => $ipaddr,                         # 接口调用机器的IP
            'notify_url'            => 'https://goods.80mall.net/callback/jlnotify?sid='.BootStarup::$sid,       # 回调通知
        ];

        $result = static::call('/api/pay/qrcodepay', $biz);
        if('00' == $result['ret_code']){
            if(1 == $result['status']){
                static::$qrcodepay_cached[$out_trade_no]  = $channel = new \swoole\Coroutine\Channel(1);
                $callback(['success' => true, 'message'=>'等待用户付款....', 'data' => ['timeout'=>$abs_timeout - time(), 'out_trade_no'=>$out_trade_no, 'qrcode'=>$result['code_url']]]);
                $pay = $channel->pop($abs_timeout - time());
                if($pay instanceof static){
                    $channel->close();
                    return $pay;
                }

                if($channel->errCode == SWOOLE_CHANNEL_TIMEOUT){
                    unset(static::$qrcodepay_cached[$out_trade_no]);
                    if(empty(static::$qrcodepay_cached)){
                        static::$qrcodepay_cached = [];
                    }
                    $channel->close();
                    throw new PayException('等待支付超时.', 1003);
                }
                $channel->close();
                throw new PayException('操作已取消.', 1004);
            }
            throw new PayException($result['ret_msg'], 1005);
        }
        return null;
    }

    /**
     * 超时后为了防止用户继续支付. 执行该接口以阻止.
     */
    public function cancel(string $ipaddr) : bool
    {
        $biz = [
            'ori_out_trade_no'=>$this->trade_no,
            'out_trade_no'=>    \lib\Utils::genOrderNo('R'),
            'mch_create_ip'=>$ipaddr
        ];

        if(null !== ($result = static::call('/api/pay/cancel', $biz))){
            $status = $result['status'] ?? -1;
            if($status == 2){
                if(null !== ($changed = $this->update(['stage'=>static::STAGE_CLOSED])))
                $db = new \sys\Db();
                $db->table('tp_nb_pays_xin')->where(['id','=', $this->id])->update($changed);
                return true;
            }else{
                $this->errmsg = $result['ret_msg'];
            }
        }
        return false;
    }

    /**
     * 查询订单支付结果, 并更新到付款记录中.
     * @access public
     */
    public function query(?\sys\Db $db = null) : bool {
        # 接口地址
        #   生产环境：https://qrcode.jlpay.com/api/pay/chnquery
        #   测试环境：https://qrcode-dev.jlpay.com/api/pay/chnquery
        $post = [
            'out_trade_no'=>$this->trade_no
        ];

        $result = static::call('/api/pay/chnquery', $post);
        if(isset($result['status'])){ // ret_code 00 ZZ 都有可能
            switch($result['status']){
            case 1: # 待确认
                if($this->stage != static::STAGE_PAYING){
                    $update = ['stage'=>static::STAGE_PAYING];
                }
                break;
            case 2: # 成功付款
                if($this->stage !== static::STAGE_PAID){
                    $update = [
                        'stage'=>static::STAGE_PAID,
                        'out_trade_no'=>$result['transaction_id'],
                        'chn'=> $result['pay_type'],
                        'buyer_id'=> $result['sub_openid'] ?? ''
                    ];
                }
                break;
            case 3: # 失败(需要撤销订单)
                if($this->stage !== static::STAGE_CLOSED){
                    $update = [
                        'stage'=>static::STAGE_CLOSED,
                    ];
                }
                break;
            case 4: # 已撤销
                if($this->stage !== static::STAGE_CLOSED){
                    $update = [
                        'stage'=>static::STAGE_CLOSED,
                    ];
                }
                break;
            case 5: # 已退款 只要有退款 就会是 5 
                if($this->stage !== static::STAGE_REFUND){
                    $update = [
                        'stage'=>static::STAGE_REFUND,
                    ];
                }
                break;
            }

            # 更新数据库
            if(isset($update)){
                $this->update($update);
                $db = $db ?? new \sys\Db();
                $db->table('tp_nb_pays_xin')->where(['id','=', $this->id])->update($update);
            }
            return true;
        }
        return false;
    }

    public function useSql(\sys\Db $db, int $order_type, \sys\db\ExpValue|int $order_id) :\sys\db\SubQuery {
        $this->lock();
        $sql = $db->table('tp_nb_pays_xin')->where([['id','=', $this->id]])->subUpdate([
            'order_type'=>$order_type,
            'order_id'=>$order_id
        ]);
        $this->unlock();
        return $sql;
    }

    public function use(int $order_type,int $order_id) : bool {
        $this->lock();
        $this->order_type = $order_type;
        $this->order_id = $order_id;
        $this->unlock();
        return true;
    }

    /**
     * 完成支付的交易进行退款操作
     */
    public function refund(?\sys\Db $db, float $amount, string $ipaddr, string $remark = '退款') : bool {
        # 接口地址
        #   生产环境：https://qrcode.jlpay.com/api/pay/refund
        #   测试环境：https://qrcode-dev.jlpay.com/api/pay/refund
        
        $this->lock();
        $allow_refund = $this->amount - $this->refund_amount;
        if($allow_refund - $amount < -0.00001){
            $this->unlock();
            throw new PayException("退款金额超过支付金额. {可退: {$allow_refund}, 当前: {$amount}}", 1005);
        }

        if($this->refund_times >= 1295){
            $this->unlock();
            throw new PayException('退款次数超出最大次数限制.', 1005);
        }

        # 退款单号 比付款单号长2位 36进制标识 最多 可以有 1295 次退款.
        # $refund_trade_no = 'R' . substr($this->trade_no, 1) . str_pad(base_convert(1 + $this->refund_times, 10, 36), 2, '0', STR_PAD_LEFT);
        

        $biz = [
            'ori_out_trade_no'=>$this->trade_no,        # 交易单号
            'total_fee'=>PaySign::IS_DEV_MODE ? '1'     # 总金额(单位:分)
                        : strval(round(100 * $amount)), #
            'mch_create_ip'=>$ipaddr,                   # 请求IP
            'remark'=>$remark
        ];

        # 这里启动退款操作(出错等待20秒后重试)
        Coroutine::create(function() use($biz, $amount) {
            $this->take();
            while(true){
                try{
                    $biz['out_trade_no']  = \lib\Utils::genOrderNo('C');
                    if(null !== ($result = static::call('/api/pay/refund', $biz))){
                        if(PaySign::verify($result)){
                            echo "验签成功\n";
                        }else{
                            Log::write("退款失败! 验签失败", 'ERROR');
                            $this->unlock();
                            return false;
                        }
                        $status = intval($result['status']) ?? -1;
                        if(2 == $status){
                            $db = new \sys\Db();
                            $db->table('tp_nb_pays_xin')
                                ->where(['id','=', $this->id])
                                ->inc('refund_times', 1)
                                ->inc('refund_amount', $amount)
                                ->update(['stage'=> static::STAGE_REFUND]);
                            $db = null;
                            $this->refund_amount += $amount;
                            $this->refund_times ++;
                            $this->unlock();
                            return true;
                        }elseif(3 == $status){
                            Log::write("退款失败! ID= {$this->id}, 嘉联返回错误: {$result['ret_msg']}", 'ERROR');

                            if($result['ret_code'] === 'C7'){
                                Coroutine::sleep(10);
                                continue;
                            }
                            $db = new \sys\Db();
                            $db->table('tp_nb_pays_xin')
                                ->where(['id','=', $this->id])
                                ->update(['stage'=>static::STAGE_REFUND_ERROR]);
                            $this->unlock();
                            return false;
                        }else{
                            $db =new \sys\Db();
                            $db->table('tp_nb_pays_xin')
                                ->where(['id','=', $this->id])
                                ->inc('refund_times', 1)
                                ->inc('refund_amount', $amount)
                                ->update(['stage'=>static::STAGE_REFUND]);
                            $this->refund_times ++;                
                            $this->unlock();
                            $this->errmsg = $result['ret_msg'];
                            return false;         
                        }
                    }else{
                        echo "调用接口失败\n";
                        return false;
                    }
                }catch(\Throwable $e){
                    $err = [
                        'Error:'.$e->getMessage().' at file:'.$e->getFile().'('.$e->getLine().')',
                        "TraceBack:\n".$e->getTraceAsString()
                    ];
                    Log::write(implode("\n", $err), 'ERROR');
                    $this->unlock();
                    return false;
                }
            }
        });
        return true;
    }

    protected static function call(string $url, array $biz, ?array $cert = null) :?array{

        $url = static::$base_url . $url;
        $payload = array_merge([
            'org_code'=>static::$org_code,
            'mch_id'=>static::$mch_id,
            'nonce_str'=>nonceStr(20),
            'sign_type'=>'RSA',
            'charset'=>'UTF-8'
        ], $biz);
    
        $payload['sign'] = PaySign::sign($payload, Pay::SRC_JLPAY);
        $post_str = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        

        Log::write("开始向嘉联网关发送支付请求", 'INFO');
        $return = http_post($url, $post_str, [
            'Content-Type'=>'text/json; charset=utf-8'
        ], 30, $cert);

        $ret_str = json_encode($return, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

        Log::write(">>>>>>>>=============================\n{$url}\n=====================================\n{$post_str}\n=====================================\n{$ret_str}\n=====================================\n", 'INFO');
        if(200 == $return['code']){
            $result = json_decode($return['body'], true);
            if(true === helpers\PaySign::verify($result)){
                Log::write("嘉联支付 验签通过!", 'INFO');
                return $result;
            }
            Log::write("嘉联支付 验签失败!", 'INFO');
        }
        throw new PayException("调用嘉联网关接口失败. {$return['code']}", 1001);
    }
}